﻿
// ==============================================================================
// 
//     Filename: CacheEnabledPage.cs
//  Description: 拥有一系列缓存方案的基础页的类定义，需要应用缓存技术的Asp.net页面可以继
//               承该类以达到构建缓存的目的。
// 
// ==============================================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Web.UI;
using DistributedUtil.Helper;

namespace DistributedWeb.BasePage {
    /// <summary>
    /// 启用缓存的Asp.net页
    /// </summary>
    public class CacheEnabledPage : Page {
        /// <summary>
        /// 是否启用缓存配置
        /// </summary>
        private static string _cachedPageEnabled = ConfigHelper.As_GetValue( "cachedPageEnabled" );
        // 缓存路径配置
        private static string _cachedPagePath = ConfigHelper.As_GetValue( "cachedPagePath" );
        // 捕获有效时间内容
        private readonly Regex _expiryRegex = new Regex( "<!-- expiry\\[([\\s\\S]+?)\\] -->", RegexOptions.Compiled);
        /// <summary>
        /// 页面缓存时间（单位：分钟）
        /// </summary>
        protected int CachedExpiryMinutes { get; private set; }
        /// <summary>
        /// 当请求参数中的参数名称包含在参数过滤列表中时，不使用缓存
        /// </summary>
        protected List<string> ParamsFilter { get; set; }
        /// <summary>
        /// 获取一个值指示当前页面是否可以被缓存
        /// </summary>
        protected bool CanCache { get; private set; }
        /// <summary>
        /// 获取一个值指示当前页面的缓存路径
        /// </summary>
        protected string CachedPagePath { get; private set; }
        /// <summary>
        /// 获取一个值指示当前页面的缓存文件名称
        /// </summary>
        protected string CachedPageFileName { get; private set; }

        public CacheEnabledPage() {
            this.CachedExpiryMinutes = 1440;
            this.ParamsFilter = new List<string>();
            this.CanCache = false;
        }

        /// <summary>
        /// 检查请求参数中是否与参数过滤列表有交集
        /// </summary>
        /// <returns></returns>
        private bool CheckFilter() {
            return this.Request.Params.AllKeys.Length > 0
                   && this.ParamsFilter.Count > 0
                   && this.Request.Params.AllKeys.Intersect( this.ParamsFilter ).Any();
        }

        // 当页面初始化时执行
        protected override void OnPreInit( EventArgs e ) {
            this.CachedPagePath = _cachedPagePath;
            this.CachedPageFileName = this.Request.Path.Replace( '/', '_' ) + ".html";
            this.CachedPagePath += HashHelper.GetHashID( this.CachedPageFileName ) + "\\";

            if ( "true" == _cachedPageEnabled && !this.IsPostBack && !this.CheckFilter()) {
                var cachedPageFileFullName = this.CachedPagePath + this.CachedPageFileName;
                if ( File.Exists( cachedPageFileFullName ) ) {
                    // 读取缓存文件中的第一行数据，第一行数据包含页面有效期数据
                    using ( var fs = File.OpenRead( cachedPageFileFullName ) ) {
                        var streamReader = new StreamReader( fs );
                        var expiryText = streamReader.ReadLine();
                        streamReader.Close();
                        streamReader.Dispose();
                        // 检查缓存文件是否过期，如果没有过期则直接向客户端发送缓存页面内容
                        if ( !string.IsNullOrWhiteSpace( expiryText ) && this._expiryRegex.IsMatch( expiryText ) ) {
                            var expiryMatch = this._expiryRegex.Match( expiryText );
                            var expiryDateTime = Convert.ToDateTime( expiryMatch.Groups[ 1 ].Value );
                            if ( expiryDateTime >= DateTime.Now ) {
                                this.Response.WriteFile( this.CachedPagePath + this.CachedPageFileName );
                                this.Response.End();
                            } else {
                                this.CanCache = true;
                            }
                        } else {
                            this.CanCache = true;
                        }
                    }
                } else {
                    this.CanCache = true;
                }
            }

            // 如果本次请求是基于Asp.net的回传机制的话就不做过多的处理了
            base.OnPreInit( e );
        }

        // 当页面发生错误时执行
        protected override void OnError( EventArgs e ) {
            base.OnError( e );

            // 如果页面发生未处理的错误，则使用已缓存的内容响应到客户端
            //var cachedPageFileFullName = this.CachedPagePath + this.CachedPageFileName;
            //if ( File.Exists( cachedPageFileFullName ) ) {
            //    this.Response.Clear();
            //    this.Response.WriteFile( this.CachedPagePath + this.CachedPageFileName );
            //    this.Response.End();
            //}
        }

        // 呈现页面内容
        protected override void Render( HtmlTextWriter writer ) {
            // 缓存页面内容
            if ( this.CanCache ) {
                // 将底层构建的内容都拦截到自定义的流中
                var stringWriter = new StringWriter();
                var htmlTextWriter = new HtmlTextWriter( stringWriter );
                // 这句话很重要，页面所有的元素要呈现为HTML内容都要经过它本身的Render()函数
                // 而这个 htmlTextWriter 就贯穿于流程始末，拦截到所有元素呈现的内容
                // 否则页面呈现的内容一个字符都获取不到
                //通过base.Render()方法，把生成的HTML写入到我们定义的htmlTextWriter中 
                base.Render(htmlTextWriter);//这是默认的Render实现，直接输出到htmlTextWriter中 
                htmlTextWriter.Flush();
                htmlTextWriter.Close();
                // 这句话比上面那句 base.Render( htmlTextWriter ); 更重要。
                // 原因就是，如果你想让页面呈现内容一字不差的发送到客户端就是这句话在起作用
                //写入到writer中，以输出页面的HTML 
                writer.Write( stringWriter );

                // 以下代码将由底层构建的用于呈现到客户端的内容保存到物理文件中，很简单
                var htmlText = stringWriter.ToString();
                stringWriter.Close();
                stringWriter.Dispose();

                // 保存页面最终呈现的内容到文件，并附加缓存有效期
                // 好吧，这里使用文件的共享方式来控制对文件的并发写入控制
                // 所以这里必须处理好异常，虽然不能写入但是至少我能做到让上面的代码呈现的页面内容顺利的响应到客户端
                try
                {
                    if ( !Directory.Exists( this.CachedPagePath ) )
                    {
                        Directory.CreateDirectory( this.CachedPagePath );
                    }

                    using ( var fs = File.Open( this.CachedPagePath + this.CachedPageFileName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read ) )
                    {
                        fs.SetLength( 0 ); // 清空原有文件的内容
                        var streamWriter = new StreamWriter( fs, Encoding.UTF8 );
                        streamWriter.WriteLine( "<!-- expiry[{0}] -->", DateTime.Now.AddMinutes( this.CachedExpiryMinutes ).ToString( "yyyy-MM-dd HH:mm:ss" ) );
                        streamWriter.Write( htmlText );
                        streamWriter.Flush();
                        streamWriter.Close();
                    }
                }
                catch ( IOException )
                {
                    // 隐藏掉由于IO操作导致的异常，例如：并发写文件因为不共享写入导致的错误。
                }
            } else {
                base.Render( writer );
            }
        }
    }
}